/*
 * 
 */
package hydrap2p.protocol.http;

import hydrap2p.download.DownloadProtocol;
import hydrap2p.download.Downloader;
import hydrap2p.download.Source;
import hydrap2p.helper.ServiceConnector;
import hydrap2p.helper.ThriftServerThread;
import hydrap2p.helper.ThriftUtils;
import hydrap2p.library.FileNotFound;
import hydrap2p.library.IOProblem;
import hydrap2p.library.InvalidUID;
import hydrap2p.library.Library;
import hydrap2p.library.Range;
import hydrap2p.logging.Level;
import hydrap2p.logging.Logging;
import hydrap2p.modulemanager.ModuleControl;
import hydrap2p.servicemanager.NameTaken;
import hydrap2p.speedlimiter.SpeedLimiter;

import java.io.BufferedInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.BindException;
import java.net.ConnectException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.List;
import java.util.Properties;

import org.apache.log4j.PropertyConfigurator;
import org.apache.thrift.TException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.Marker;
import org.slf4j.MarkerFactory;

/**
 * Implementation of HTTP for HydraP2P.
 * 
 * @author Phil
 */
public class HttpProtocol implements DownloadProtocol.Iface, ModuleControl.Iface {
	static {
		//Configure log4j
		Properties log4jProps = new Properties();
		log4jProps.setProperty("log4j.rootLogger", "TRACE, console");
		log4jProps.setProperty("log4j.appender.console", "org.apache.log4j.ConsoleAppender");
		log4jProps.setProperty("log4j.appender.console.layout", "org.apache.log4j.PatternLayout");
		log4jProps.setProperty("log4j.appender.console.layout.ConversionPattern", "%-4r [%t] %-5p %c %x - %m%n");

		PropertyConfigurator.configure(log4jProps);
	}
	
	// Class string name, doubling as logging marker
	private static final Marker TAG = MarkerFactory.getDetachedMarker("HttpProtocol");

	// Console logger - used before connections established
	private static final Logger consoleLogger = LoggerFactory.getLogger(TAG.getName());

	private static ThriftServerThread t;

	private static final String PREFIX = "http://";
	
	private int port;

	public HttpProtocol(int port) {
		this.port = port;
	}

	/**
	 * The main method.
	 * 
	 * @param args the arguments
	 */
	public static void main(String[] args) {
		// Tell Manager about HttpProtocol's existence
		int port = 34655;
		HttpProtocol httpProtocol = null;

		try {
			httpProtocol = new HttpProtocol(port);
		} catch(Exception e) {
			System.out.println("Failed to start HttpProtocol, " + e.getMessage());
			return;
		}

		// Start server
		consoleLogger.info(TAG, "Starting HTTP server");
		
		try {
			t = ThriftUtils.initThriftServer(new DownloadProtocol.Processor(httpProtocol), "HttpProtocol", port);
		} catch (BindException e) {
			consoleLogger.error(TAG, "Failed to start HttpProtocol!  Could not bind to port.");
			e.printStackTrace();
			return;
		} catch (IOException e) {
			consoleLogger.error(TAG, "Failed to start HttpProtocol!");
			e.printStackTrace();
			return;
		} catch (NameTaken e) {
			consoleLogger.error(TAG, "Failed to start HttpProtocol!  Duplicate module exists.");
			e.printStackTrace();
			return;
		}
		
		//Ensure module connections
		boolean manager = ThriftUtils.waitForManager();
		boolean library = ThriftUtils.waitForLibrary();
		boolean downloader = ThriftUtils.waitForDownloader();
		boolean logging = ThriftUtils.waitForLogging();
		
		if(manager && library && downloader) {
			try {
				Downloader.Client downloaderModule = ServiceConnector.getDownloader();
				downloaderModule.addDownloadProtocol(t.getLocation(), "http");
			} catch (TException e) {
				consoleLogger.error(TAG, "Failed to register with downloader!");
				return;
			}
			
			t.start();
		}
		else
			consoleLogger.error(TAG, "Failed to connect to essential modules!  Exiting.");
	}
	
	/* (non-Javadoc)
	 * @see hydrap2p.download.DownloadProtocol.Iface#cancelDownload(long, hydrap2p.download.Source)
	 */
	@Override
	public void cancelDownload(long uid, Source source) throws TException {

	}
	
	public void download(long uid, Source source, Range range, Downloader.Iface downloader, Library.Iface library)
	{
		SpeedLimiter.Client speedlimiter = ServiceConnector.getSpeedLimiter();
		
		try {
			log(Level.DEBUG, "Download starting - HTTP");
			if(source == null || source.getProtocol() == null) {
				downloader.erroredDownloading(uid, source, range, true);
				log(Level.ERROR, "Invalid source passed to Http!");
				return;
			} else if(!source.getProtocol().equalsIgnoreCase("http")) {
				downloader.erroredDownloading(uid, source, range, true);
				log(Level.ERROR, "Source with incorrect protocol passed to Http!");
				return;
			}
		} catch(TException e) {
			log(Level.ERROR, "Downloader is not available, even after telling me what to do. Very strange.");
			return;
		}

		HttpURLConnection conn = null;
		BufferedInputStream dlStream = null;
		byte[] data = null;
		data = new byte[(int)range.len];

		String path = null;
		try {
			if(source.getExtra() != null) {
				path = source.getExtra().get("path");
	
				if(path == null) {
					log(Level.ERROR, "Path extra does not exist in source!");
					downloader.removeSource(uid, source);
					return;
				}
			} else {
				log(Level.ERROR, "Source extras are null!");
				downloader.removeSource(uid, source);
				return;
			}
		} catch(TException e) {
			log(Level.ERROR, "Could not tell the downloader to remove an invalid source");
			return;
		}

		try {
			try {
				String port = "";
				//If port has been set, else omit
				if(source.port != 0)
					port = String.valueOf(source.port);

				URL url;
				url = new URL(PREFIX + source.ip + ":" + port + path);
				conn = (HttpURLConnection)url.openConnection();
			} catch(MalformedURLException e) {
				log(Level.ERROR, "Malformed url in source");
				e.printStackTrace();
				downloader.erroredDownloading(uid, source, range, true);
				return;
			}
			
			log(Level.DEBUG, "Download initialization complete, connecting...");
			
			conn.setRequestProperty("Range", "bytes=" + range.start + "-" + (range.start + range.len));
			
			// Load extra headers from the source
			if (source.getExtra().containsKey("extraheaders"))
			{
				String[] splits=source.getExtra().get("extraheaders").split("\n");
				for (String extraheader : splits)
				{
					if (source.getExtra().containsKey(extraheader))
					{
						log(Level.VERBOSE, "Adding extra header "+extraheader+": "+source.getExtra().get(extraheader));
						conn.setRequestProperty(extraheader, source.getExtra().get(extraheader));
					}
					else
						log(Level.MISHAP, "HTTP Source wants to use extra header "+extraheader+" but the extra data was not found");
				}
			}
			/*
			for (Entry<String, List<String>> entry: conn.getRequestProperties().entrySet())
			{
				System.err.println(entry.getKey()+": "+entry.getValue().get(0));
			}
			for (Entry<String, List<String>> entry: conn.getHeaderFields().entrySet())
			{
				System.err.println(entry.getKey()+": "+entry.getValue().get(0));
			}
			System.err.println();
			*/
			
			conn.connect();
			if (conn.getContentLength() > 0 && conn.getContentLength()<range.len)
			{
				log(Level.MISHAP, "Web server returned smaller data than requested! Wanted "+range.len+" and server offered "+conn.getContentLength());
				/*
				BufferedReader reader=new BufferedReader(new InputStreamReader(conn.getErrorStream()));
				String output="Data from the server: ";
				String temp="";
				while (temp!=null)
				{
					temp=reader.readLine();
					if (temp!=null)
						output=output+"\n"+temp;
				}
				log(Level.VERBOSE, output);
				*/
				downloader.erroredDownloading(uid, source, range, true);
				return;
			}
			dlStream = new BufferedInputStream(conn.getInputStream());

			log(Level.DEBUG, "Done!");
			
			//Get data
			int bytesRead = 0; 
			int offset = 0;
			int loopCounter = 0;	//Used to throttle log output
			//Loop until we've gotten range.len bytes
			while (bytesRead < range.len && (offset = dlStream.read(data, bytesRead, data.length - bytesRead)) >= 0) { 
				bytesRead += offset; 
				
				if (speedlimiter!=null)
				{
					try {
						speedlimiter.attempt(source, true, offset);
					}
					catch (Exception e)
					{
						speedlimiter=null;
					}
				}

				try {
					library.saveData(uid, range.start+bytesRead-offset, ByteBuffer.wrap(data, bytesRead-offset, offset));
				} catch(FileNotFound e) {
					//Problem saving data
					e.printStackTrace();
					downloader.erroredDownloading(uid, source, range, false);
					return;
				} catch(IOProblem e) {
					//Problem saving data
					e.printStackTrace();
					downloader.erroredDownloading(uid, source, range, false);
					return;
				} catch (InvalidUID e) {
					//Problem saving data
					e.printStackTrace();
					downloader.erroredDownloading(uid, source, range, false);
					return;
				}
				
				loopCounter++;
				if(loopCounter % 5 == 0)
					log(Level.VERBOSE, "Download chunk " + range.start + "-" + (range.start+range.len) + " " + (float)bytesRead/(float)data.length * 100.0f + "% complete");
			}

			if(bytesRead != range.len) {
				log(Level.MISHAP, "Mismatch between bytes requested and bytes downloaded!");
				
				downloader.erroredDownloading(uid, source, range, true);
				return;
			}
			
			downloader.finishedDownloading(uid, source, range);

			//TODO: Verify data
			log(Level.INFO, "Successfully downloaded " + bytesRead + " bytes of data");
		} catch(IOException e) {
			//Something horrible happened during the download
			log(Level.ERROR, "IOException downloading data!");
			e.printStackTrace();
			try {
				if (e instanceof FileNotFoundException)
					downloader.erroredDownloading(uid, source, range, true);
				else if (e.getMessage().contains("Server returned HTTP response code: 403"))
					downloader.erroredDownloading(uid, source, range, true);
				else if (e instanceof ConnectException)
					downloader.erroredDownloading(uid, source, range, true);
				else
					downloader.erroredDownloading(uid, source, range, false);
				return;
			} catch (TException e1) {
				e1.printStackTrace();
				log(Level.ERROR, "A communication error happened with a required module!");
				return;
			}
		} catch (TException e) {
			e.printStackTrace();
			log(Level.ERROR, "A communication error happened with a required module!");
			return;
		
		} finally {
			//Close stream
			if(dlStream != null) {
				try {
					dlStream.close();
				} catch(IOException e) {
					e.printStackTrace();
				}
			}

			//Close connection
			if(conn != null) {
				conn.disconnect();
			}
		}

		return;
	}

	/* (non-Javadoc)
	 * @see hydrap2p.download.DownloadProtocol.Iface#download(long, hydrap2p.download.Source, hydrap2p.library.Range)
	 */
	@Override
	public void download(long uid, Source source, Range range) throws TException {
		//TODO Wait for each and check for validity
		Downloader.Client downloader = ServiceConnector.getDownloader();
		Library.Client library = ServiceConnector.getLibrary();
		download(uid, source, range, downloader, library);
	}

	/* (non-Javadoc)
	 * @see hydrap2p.Service.Iface#ping()
	 */
	@Override
	public void ping() throws TException {

	}

	/* (non-Javadoc)
	 * @see hydrap2p.Service.Iface#supportedInterfaces()
	 */
	@Override
	public List<String> supportedInterfaces() throws TException {
		return Arrays.asList("DownloadProtocol", "ModuleControl");
	}

	/**
	 * Log.
	 * 
	 * @param severity the severity
	 * @param logMessage the log message
	 */
	protected void log(Level severity, String logMessage) {
		Logging.Client logging = ServiceConnector.getLogger();
		
		//Log to Logging module if possible, and fall back to console.
		try {
			logging.log(severity, TAG.toString(), logMessage);
		} catch(TException e) {
			logToSystemError(severity, logMessage);
		} catch(NullPointerException e) {
			logToSystemError(severity, logMessage);
		}
	}

	/**
	 * Log to system error.
	 * 
	 * @param severity the severity
	 * @param logMessage the log message
	 */
	private void logToSystemError(Level severity, String logMessage) { 
		//Log to system error.  If they can't be arsed to use a Logging constant as severity, the message gets logged at trace level
		switch(severity) {
		case VERBOSE:
			consoleLogger.trace(TAG, logMessage);
			break;
		case DEBUG:
			consoleLogger.debug(TAG, logMessage);
			break;
		case INFO:
			consoleLogger.info(TAG, logMessage);
			break;
		case MISHAP:
			consoleLogger.warn(TAG, logMessage);
			break;
		case ERROR:
			consoleLogger.error(TAG, logMessage);
			break;
		default:
			consoleLogger.trace(TAG, logMessage);
		}
	}

	/* (non-Javadoc)
	 * @see hydrap2p.manager.ModuleControl.Iface#stop()
	 */
	@Override
	public void stop() throws TException {
		consoleLogger.info(TAG, "Stopping HTTP server");
		
		t.shutdown();
	}
}
